package ace.module.oauth2.server.core.impl.authentication.ace.handler.impl;

import ace.cmp.core.exception.BusinessException;
import ace.cmp.core.model.R;
import ace.cmp.json.api.JsonService;
import ace.cmp.spring.util.core.ValidatorUtils;
import ace.module.oauth2.server.api.callback.Oauth2AceAuthenticationCallbackApi;
import ace.module.oauth2.server.api.callback.model.input.Oauth2AceAuthenticationCallbackInput;
import ace.module.oauth2.server.api.enums.Oauth2RegisteredClientAuthConfigAuthenticationTypeEnum;
import ace.module.oauth2.server.api.model.dto.ext.Oauth2AceAuthResultDetailDto;
import ace.module.oauth2.server.core.impl.authentication.ace.Oauth2AceAuthenticationToken;
import ace.module.oauth2.server.core.impl.authentication.ace.factory.CallbackApiFactory;
import ace.module.oauth2.server.core.impl.authentication.ace.handler.Oauth2AceAuthenticationHandler;
import ace.module.oauth2.server.core.impl.dao.entity.Oauth2RegisteredClientAuthConfig;
import ace.module.oauth2.server.core.impl.manager.Oauth2RegisteredClientAuthConfigManager;
import java.util.AbstractMap.SimpleEntry;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.OAuth2Error;
import org.springframework.stereotype.Component;

/**
 * @author caspar
 * @date 2023/2/8 13:43 rest 登录
 */
@AllArgsConstructor
@Slf4j
@Component
public class Oauth2AceAuthenticationHandlerImpl implements Oauth2AceAuthenticationHandler {

  private static final String AUTHENTICATION_METHOD_NAME = "authentication";
  private final ValidatorUtils validatorUtils;
  private final JsonService jsonService;
  private final Oauth2RegisteredClientAuthConfigManager oauth2RegisteredClientAuthConfigManager;
  private final CallbackApiFactory callbackApiFactory;


  @Override
  public R<Oauth2AceAuthResultDetailDto> authentication(Oauth2AceAuthenticationToken token) {
    try {
      R<Oauth2AceAuthResultDetailDto> result = this.doAuthentication(token);
      return result;
    } catch (OAuth2AuthenticationException ex) {
      throw ex;
    } catch (BusinessException ex) {
      throw ex;
    } catch (Exception ex) {
      log.error("invoke ace authentication service fail", ex);
      throw ex;
    }
  }

  protected R<Oauth2AceAuthResultDetailDto> doAuthentication(Oauth2AceAuthenticationToken token) {
    List<Oauth2RegisteredClientAuthConfig> oauth2RegisteredClientAuthConfigs = this.getFindOrDefaultOauth2RegisteredClientAuthConfigBy(token);
    if (CollectionUtils.isEmpty(oauth2RegisteredClientAuthConfigs)) {
      throw new BusinessException(
          String.format("没有配置[%s]的authType认证类型", token.getBodyModel().getAuthType())
      );
    }
    R<Oauth2AceAuthResultDetailDto> result = null;

    List<Entry<Oauth2RegisteredClientAuthConfig, Oauth2AceAuthenticationCallbackApi>> grantTypeAndApis = this.getOrCreateCallbackApi(oauth2RegisteredClientAuthConfigs);

    for (Entry<Oauth2RegisteredClientAuthConfig, Oauth2AceAuthenticationCallbackApi> grantTypeAndApi : grantTypeAndApis) {
      Oauth2AceAuthenticationCallbackApi api = grantTypeAndApi.getValue();

      String idOfClient = token.getOauth2ClientAuthenticationToken().getRegisteredClient().getId();

      Oauth2AceAuthenticationCallbackInput requestModel = this.convertCallbackRequest(token, idOfClient, api);

      R<Oauth2AceAuthResultDetailDto> newResult = api.authentication(requestModel);

      result = this.checkAndMergeAuthenticationResultRule(result, newResult);

    }
    return result;
  }


  protected Oauth2AceAuthenticationCallbackInput convertCallbackRequest(
      Oauth2AceAuthenticationToken token,
      String idOfClient,
      Oauth2AceAuthenticationCallbackApi api
  ) {
    String bodyString = token.getBodyString();
    // 根据认证方式进行参数验证
    Oauth2AceAuthenticationCallbackInput bodyModel = this.jsonService.toObject(bodyString, Oauth2AceAuthenticationCallbackInput.class);
    bodyModel.setIdOfClient(idOfClient);
    bodyModel.setAdditionalParameters(token.getAdditionalParameters());
    validatorUtils.validateThrowFirst(bodyModel);

    return bodyModel;
  }

  protected List<Entry<Oauth2RegisteredClientAuthConfig, Oauth2AceAuthenticationCallbackApi>> getOrCreateCallbackApi(List<Oauth2RegisteredClientAuthConfig> oauth2RegisteredClientAuthConfigs) {
    return oauth2RegisteredClientAuthConfigs.stream()
        .map(oauth2RegisteredClientGrantType -> {
          Oauth2AceAuthenticationCallbackApi api = this.callbackApiFactory.getOrCreateAuthCallbackApi(oauth2RegisteredClientGrantType);
          Entry<Oauth2RegisteredClientAuthConfig, Oauth2AceAuthenticationCallbackApi> entry = new SimpleEntry<>(oauth2RegisteredClientGrantType, api);
          return entry;
        })
        .collect(Collectors.toList());
  }

  private List<Oauth2RegisteredClientAuthConfig> getFindOrDefaultOauth2RegisteredClientAuthConfigBy(Oauth2AceAuthenticationToken token) {
    return this.getFindOrDefaultOauth2RegisteredClientAuthConfigBy(
        token.getOauth2ClientAuthenticationToken().getRegisteredClient().getId(),
        token.getBodyModel().getAuthType()
    );
  }

  private List<Oauth2RegisteredClientAuthConfig> getFindOrDefaultOauth2RegisteredClientAuthConfigBy(String idOfClient, String authType) {
    List<Oauth2RegisteredClientAuthConfig> allOauth2RegisteredClientAuthConfigs = this.oauth2RegisteredClientAuthConfigManager
        .getByIdOfClientAndStatusIsEnableAndBizAuthTypeIsAuthenticationTypeFromDbOrCache(idOfClient);

    List<Oauth2RegisteredClientAuthConfig> findOrDefaultOauth2RegisteredClientAuthConfigs = this.oauth2RegisteredClientAuthConfigManager
        .getByAuthTypeFromMemory(allOauth2RegisteredClientAuthConfigs, authType);

    if (CollectionUtils.isEmpty(findOrDefaultOauth2RegisteredClientAuthConfigs)) {
      String defaultAuthType = Oauth2RegisteredClientAuthConfigAuthenticationTypeEnum.DEFAULT.getCode();
      findOrDefaultOauth2RegisteredClientAuthConfigs = this.oauth2RegisteredClientAuthConfigManager.getByAuthTypeFromMemory(allOauth2RegisteredClientAuthConfigs, defaultAuthType);
    }

    List<Oauth2RegisteredClientAuthConfig> result = Optional.ofNullable(findOrDefaultOauth2RegisteredClientAuthConfigs)
        .stream()
        .flatMap(List::stream)
        .sorted(Comparator.comparingLong(Oauth2RegisteredClientAuthConfig::getPriority).reversed())
        .collect(Collectors.toList());

    return result;
  }

  /**
   * 检查认证结果,并且合并认证信息.
   *
   * @param authenticationResult 上次认证结果
   * @param newAuthenticationResult 最新认证结果
   * @return
   */
  private R<Oauth2AceAuthResultDetailDto> checkAndMergeAuthenticationResultRule(
      R<Oauth2AceAuthResultDetailDto> authenticationResult,
      R<Oauth2AceAuthResultDetailDto> newAuthenticationResult
  ) {

    this.checkAuthenticationResultCommonRule(newAuthenticationResult);

    if (authenticationResult != null && !StringUtils.equals(newAuthenticationResult.getData().getAccountId(), authenticationResult.getData().getAccountId())) {
      throw new OAuth2AuthenticationException("认证失败,原因：多次认证-账户id不相等.");
    }

    if (authenticationResult == null) {
      authenticationResult = newAuthenticationResult;
    } else {
      if (MapUtils.isNotEmpty(newAuthenticationResult.getData().getAttributes())) {
        authenticationResult
            .getData()
            .getAttributes()
            .putAll(newAuthenticationResult.getData().getAttributes());
      }
    }

    return authenticationResult;
  }

  private void checkAuthenticationResultCommonRule(R<Oauth2AceAuthResultDetailDto> authenticationResult) {
    if (authenticationResult == null) {
      throw new OAuth2AuthenticationException("all authentication fail");
    }

    if (authenticationResult.success() == false) {
      throw new OAuth2AuthenticationException(
          new OAuth2Error(authenticationResult.getCode().toString(), authenticationResult.getMessage(), "")
      );
    }

    if (StringUtils.isEmpty(authenticationResult.getData().getAccountId())) {
      throw new OAuth2AuthenticationException("accountId must not be null");
    }
  }
}
